Skip to content
标签
服务发现
字数
2434 字
阅读时间
12 分钟

一、概述

Eureka 是 Netflix 开发的服务发现组件,本身是一个基于 REST 的服务。Spring Cloud 将它集成在其子项目 spring-cloud-netflix 中,以实现 Spring Cloud 的服务注册于发现,同时 还提供了负载均衡、故障转移等能力。

1.1 三个角色

角色功能
Eureka Server通过 Register、Get、Renew 等接口提供服务的注册和发现。
Application Service (Service Provider)服务提供方 把自身的服务实例注册到 Eureka Server 中
Application Client (Service Consumer)服务调用方 通过 Eureka Server 获取服务列表,消费服务。

1.2 高可用环境搭建

Eureka Server 高可用环境需要部署两个Eureka server,它们互相向对方注册。启动不同的端口,如下图:

image-20201115213743299

1.3 自我保护

一般情况下,微服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来 判断服务时候健康,同时会定期删除超过 90 秒没有发送心跳服务。

自我保护:Eureka 设置了一个阀值,当判断挂掉的服务的数量超过阀值时, Eureka Server 认为很大程度上出现了网络故障,将不再删除心跳过期的服务。 阈值是:15 分钟之内是否低于 85%;

1.4 元数据

Eureka的元数据有两种:标准元数据和自定义元数据。

  • 标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
  • 自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。

在程序中可以使用DiscoveryClient 获取指定微服务的所有元数据信息

java
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RestTemplateTest {
 
 @Autowired
 private DiscoveryClient discoveryClient;
 @Test
 public void test() {
 //根据微服务名称从注册中心获取相关的元数据信息
 List<ServiceInstance> instances = discoveryClient.getInstances("shopservice-product");
 for (ServiceInstance instance : instances) {
 System.out.println(instance);
 }
 }
}

二、使用Demo

2.1 服务端

依赖

groovy
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

配置

yml
server:
	port:  50101  #服务端口
	
spring:
	application:
		name: xc‐govern‐center #指定服务名
eureka:
	client:
		registerWithEureka: false #服务注册,是否将自己注册到Eureka服务中
		fetchRegistry: false #服务发现,是否从Eureka中获取注册信息
		serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机 8761 端口)
		defaultZone: http://localhost: 50101 /eureka/
	server:
	  enable‐self‐preservation: false #是否开启自我保护模式
	  eviction‐interval‐timer‐in‐ms:  60000  #服务注册表清理间隔(单位毫秒,默认是 60 * 1000 )
	instance:
	  preferIpAddress: true #使用IP注册
	  lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给server端后,续约到期时间(默认90秒)
	  lease-renewal-interval-in-seconds: 5 #发送心跳续约间
	  instance-id: ${spring.cloud.client.ip-address}:${server.port} #spring.cloud.client.ip-address:获取ip地址

启用

启动类添加@EnableEurekaServer 标识这是一个Eureka服务

2.2 客户端

配置

yml
eureka:
	client:
		registerWithEureka: true #服务注册开关
		fetchRegistry: true #服务发现开关
		serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
		defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
	instance:
		prefer‐ip‐address: true #将自己的ip地址注册到Eureka服务中
		ip‐address: ${IP_ADDRESS:127.0.0.1}
		instance‐id: ${spring.application.name}:${server.port} #指定实例id

启用

启动类添加注解@EnableDiscoveryClient ,表示它是一个Eureka的客户端

2.3 服务端获取Eureka实例相关信息

java


import com.commnetsoft.commons.Pager;
import com.commnetsoft.core.CommonError;
import eureka.model.EurekaLogPageQueryDto;
import eureka.model.InstanceInfoDto;
import eureka.model.InstanceQueryDto;
import exception.MicroRuntimeException;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.config.ConfigurationManager;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Pair;
import com.netflix.eureka.EurekaServerContext;
import com.netflix.eureka.cluster.PeerEurekaNode;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
import com.netflix.eureka.resources.StatusResource;
import com.netflix.eureka.util.StatusInfo;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author wangxp
 * @Date 2020/10/21
 */
@Service
public class MonitoringService {

    private static final Logger log = LoggerFactory.getLogger(MonitoringService.class);

    @Autowired
    private ApplicationInfoManager applicationInfoManager;
    @Autowired
    private PeerAwareInstanceRegistry registry;
    @Autowired
    private EurekaServerContext eurekaServerContext;


    /**
     * 获取eureka上注册的服务实例信息
     * @Params:[service]
     * @Return: java.util.ArrayList
     **/
    public List<Map<String, Object>> getServiceList( InstanceQueryDto queryDto){
        //获取所有实例
        List<Application> sortedApplications = registry.getSortedApplications();
        ArrayList<Map<String, Object>> maps = new ArrayList<>();
        Iterator var4 = sortedApplications.iterator();

        while(var4.hasNext()) {
            // 获取某名称下的所有实例
            Application app = (Application)var4.next();
            String appName = app.getName();
            String service = queryDto.getService();
            // 根据条件查询 名称不同跳过
            if (StringUtils.isNotBlank(service) && (!appName.toLowerCase().equals(service.toLowerCase()))){
                continue;
            }
            Map<String, Object> map = new HashMap<>();
            map.put("appName",appName);

            List<Map<String, Object>> instances = new ArrayList<>();
            Iterator var10 = app.getInstances().iterator();
            //获取信息
            while(var10.hasNext()) {
                InstanceInfo info = (InstanceInfo)var10.next();
                Map<String, Object> instanceMap = new HashMap<>();
                instanceMap.put("instanceId",info.getInstanceId());
                instanceMap.put("ipAddr",info.getIPAddr());
                instanceMap.put("statu",info.getStatus().name());
                instances.add(instanceMap);
            }
            map.put("instances",instances);
            maps.add(map);
        }
        return maps;
    }

    /**
     * 获取实例集合
     * @Params:[appName]
     * @Return: java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
     **/
    public List<Map<String, Object>> getInstanceList(String appName) {

        //获取所有实例
        List<Application> sortedApplications = registry.getSortedApplications();
        List<Map<String, Object>> instances = new ArrayList<>();

        Iterator var4 = sortedApplications.iterator();
        while(var4.hasNext()) {
            Application app = (Application)var4.next();
            String name = app.getName();
            // 根据条件查询 名称不同跳过
            if (!name.toLowerCase().equals(appName.toLowerCase())){
                continue;
            }
            Iterator var10 = app.getInstances().iterator();
            //获取信息
            while(var10.hasNext()) {
                InstanceInfo info = (InstanceInfo)var10.next();
                Map<String, Object> instanceMap = new HashMap<>();
                instanceMap.put("instanceId",info.getInstanceId());
                instanceMap.put("ipAddr",info.getIPAddr());
                instanceMap.put("statu",info.getStatus().name());
                LeaseInfo leaseInfo = info.getLeaseInfo();
                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                instanceMap.put("registrationTime",format.format(new Date(leaseInfo.getRegistrationTimestamp())));
                instanceMap.put("renewalTime",format.format(new Date(leaseInfo.getRenewalTimestamp())));
                instances.add(instanceMap);
            }
            break;
        }
        return instances;

    }

    /**
     * 获取Eureka实例的相关信息
     * @Params:[]
     * @Return: java.util.Map<java.lang.String,java.lang.Object>
     **/
    public Map<String, Object> getEurekaStatu(){
        Map<String, Object> model = new HashMap<>();
        //基本信息
        model.put("environment", ConfigurationManager.getDeploymentContext().getDeploymentEnvironment());
        model.put("datacenter", ConfigurationManager.getDeploymentContext().getDeploymentDatacenter());
        model.put("currentTime", StatusResource.getCurrentTimeAsString());
        model.put("upTime", StatusInfo.getUpTime());
        model.put("leaseExpirationEnabled",registry.isLeaseExpirationEnabled());
        model.put("numOfRenewsPerMinThreshold",registry.getNumOfRenewsPerMinThreshold());
        model.put("numOfRenewsInLastMin",registry.getNumOfRenewsInLastMin());
        model.put("selfPreservationModeEnabled",registry.isSelfPreservationModeEnabled());
        model.put("isBelowRenewThresold", registry.isBelowRenewThresold() == 1);

        //状态信息
        StatusInfo statusInfo;
        try {
            statusInfo = (new StatusResource()).getStatusInfo();
        } catch (Exception var5) {
            statusInfo = StatusInfo.Builder.newBuilder().isHealthy(false).build();
        }
        model.put("generalStats", statusInfo.getGeneralStats());
        model.put("applicationStats",statusInfo.getApplicationStats());

        // Eureka集群信息
        Map<String, String> replicas = new LinkedHashMap();
        List<PeerEurekaNode> list = eurekaServerContext.getPeerEurekaNodes().getPeerNodesView();
        Iterator var5 = list.iterator();

        while(var5.hasNext()) {
            PeerEurekaNode node = (PeerEurekaNode)var5.next();
            try {
                URI uri = new URI(node.getServiceUrl());
                String href = this.scrubBasicAuth(node.getServiceUrl());
                replicas.put(uri.getHost(), href);
            } catch (Exception var9) {
            }
        }
        model.put("replicas",replicas);

        //当前Eureka实例信息
        InstanceInfo instanceInfo = statusInfo.getInstanceInfo();
        Map<String, String> instanceMap = new HashMap();
        instanceMap.put("ipAddr", instanceInfo.getIPAddr());
        instanceMap.put("status", instanceInfo.getStatus().toString());
        model.put("instanceInfo",instanceMap);
        return model;
    }

    /**
     * 获取注销或注册日志  eureka重启后清空
     * @Params:[type] 0为 查看取消日志  1为查看注册日志
     * @Return: java.util.List
     **/
    public Pager<Map<String, Object>> getEurekaLog(EurekaLogPageQueryDto pageQueryDto){

        ArrayList<Map<String, Object>> logs = new ArrayList();
        List<Pair<Long, String>> list;
        //类型不为注销日志时,查看注册日志
        if (EurekaLogPageQueryDto.LOGOUTSTATU.equals(pageQueryDto.getType())){
            list = registry.getLastNCanceledInstances();
        }else {
            list = registry.getLastNRegisteredInstances();
        }
        int pagesize = pageQueryDto.getPagesize();
        int pagenum = pageQueryDto.getPagenum();

        //获取分页后的list集合
        List pageList = getPageList(list, pagesize, pagenum);

        Iterator var6 = pageList.iterator();

        while(var6.hasNext()) {
            Pair<Long, String> entry = (Pair)var6.next();
            logs.add(this.registeredInstance((String)entry.second(), (Long)entry.first()));
        }

        Pager<Map<String, Object>> pager = new Pager<>();
        pager.setTotal((long)list.size());
        pager.setPagenum(pagenum);
        pager.setPagesize(pagesize);
        pager.setRows(logs);
        return pager;
    }

    /**
     * 删除服务(若服务未关闭,则在下次心跳还会再次注册)
     * @Params:[appName, instanceId]
     * @Return: com.commnetsoft.commons.Result<java.lang.Void>
     **/
    public void cancelLease(InstanceInfoDto infoDto) throws MicroRuntimeException {
        String appName = infoDto.getAppName();
        String instanceId = infoDto.getInstanceId();
        boolean isSuccess;
        try {
            isSuccess = registry.cancel(appName, instanceId, false);

        } catch (Throwable e) {
            log.error("删除失败 (cancel): {} - {}", appName, instanceId, e);
            throw new MicroRuntimeException(CommonError.unknown,"删除失败");
        }
        if (isSuccess) {
            log.debug("删除成功: {} - {}", appName, instanceId);
        } else {
            log.info("未找到 (Cancel): {} - {}", appName, instanceId);
            throw new MicroRuntimeException(CommonError.notfound,"未找到实例");
        }
    }

    /**
     * 实例状态更新
     * @Params:[appName, instanceId, statu] statu为 1 暂停服务,为其他值恢复正常状态
     * @Return: com.commnetsoft.commons.Result<java.lang.Void>
     **/
    public void statusUpdate(InstanceInfoDto infoDto) throws MicroRuntimeException {
        String appName = infoDto.getAppName();
        String instanceId = infoDto.getInstanceId();
        Integer statu = infoDto.getStatu();
        //校验状态值
        if (null == statu){
            throw new MicroRuntimeException(CommonError.illegal_args,"状态值为空");
        }
        InstanceInfo.InstanceStatus instancestStatu;
        if (statu.equals(InstanceInfoDto.PAUSESTATU)){
            instancestStatu = InstanceInfo.InstanceStatus.OUT_OF_SERVICE;
        }else if (statu.equals(InstanceInfoDto.OPERATIONSTATU)){
            instancestStatu = InstanceInfo.InstanceStatus.UP;
        }else {
            throw new MicroRuntimeException(CommonError.illegal_args,"状态值不合法");
        }

        //校验实例是否存在
        if (registry.getInstanceByAppAndId(appName, instanceId) == null) {
            log.warn("未找到实例: {}/{}", appName, instanceId);
            throw new MicroRuntimeException(CommonError.notfound,"未找到实例");
        }

        boolean isSuccess;
        try {
            //对状态值进行更新
            isSuccess = registry.statusUpdate(appName, instanceId,
                    instancestStatu, null,
                    false);
        } catch (Throwable e) {
            log.error("Error updating instance {} for status {}", instanceId,
                    InstanceInfo.InstanceStatus.OUT_OF_SERVICE,e);
            throw new MicroRuntimeException(CommonError.unknown,"更新实例状态失败");
        }
        if (isSuccess) {
            log.info("更新状态成功: {} - {} - {}", appName, instanceId, instancestStatu);
        } else {
            log.warn("更新失败失败: {} - {} - {}", appName, instanceId, instancestStatu);
            throw new MicroRuntimeException(CommonError.unknown,"更新实例状态失败");
        }
    }

    /**
     * 清理配置中Eureka链接上的身份验证信息
     * @Params:[urlList]
     * @Return: java.lang.String
     **/
    private String scrubBasicAuth(String urlList) {
        String[] urls = urlList.split(",");
        StringBuilder filteredUrls = new StringBuilder();
        String[] var4 = urls;
        int var5 = urls.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String u = var4[var6];
            if (u.contains("@")) {
                filteredUrls.append(u, 0, u.indexOf("//") + 2).append(u.substring(u.indexOf("@") + 1)).append(",");
            } else {
                filteredUrls.append(u).append(",");
            }
        }
        return filteredUrls.substring(0, filteredUrls.length() - 1);
    }

    /**
     * 注册实例 id及时间
     * @Params:[id, date]
     * @Return: java.util.Map<java.lang.String,java.lang.Object>
     **/
    private Map<String, Object> registeredInstance(String id, long date) {
        HashMap<String, Object> map = new HashMap();
        map.put("id", id);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        map.put("date", format.format(new Date(date)));
        return map;
    }

    /**
     * 分页方法
     * @Return: java.util.List
     **/
    private List getPageList(List list, Integer pagesize,Integer pagenum){
        int size = list.size();
        //集合为空直接返回
        if (size == 0){
            return list;
        }
        // size大于需要数量时直接返回
        if (size >= pagesize*pagenum ){
            if (pagenum ==1){
                return list.subList(0,pagesize);
            }else {
                return list.subList((pagenum-1)*pagesize,pagesize*pagenum);
            }
        }else{
            if(pagenum == 1){
                return list.subList(0,size);
            }else{
                int i = size/pagesize;
                int j = size%pagesize;
                if (j != 0){
                    if (i ==1){
                        return list.subList(pagesize,size);
                    }else{
                        return list.subList(i*pagesize,size);
                    }
                }else{
                    if (i ==1){
                        return list.subList(0,size);
                    }else{
                        return list.subList((i-1)*pagesize,size);
                    }
                }
            }
        }
    }
}

2.4 安全注册认证

依赖

xml
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency>

配置

properties
#开启 http basic 的安全认证 
security.basic.enabled=true 
security.user.name=user 
security.user.password=123456

服务注册

properties
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/

三、常见问题

3.1 服务注册慢

服务的注册涉及到心跳,默认心跳间隔为30s。在实例、服务器、客户端都在本地缓存中具有相同的元数据之前,服务不可用于客户端发现(所以可能需要3次心跳)。可以通过配置eureka.instance.leaseRenewalIntervalInSeconds (心跳频率)加快客户端连接到其他服务的过程。在生产中,最好坚持使用默认值,因为在服务器内部有一些计算,他们对续约做出假设。

3.2 服务节点剔除

默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护的机制。所以不能有效而迅速的剔除失效节点,这对开发或测试会造成困扰。解决方案如下:

Eureka Server:配置关闭自我保护,设置剔除无效节点的时间间隔

Eureka Client:配置开启健康检查,并设置续约时间